﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration;
using System.Configuration.Provider;
using System.Security.Cryptography;
using System.Text;
using System.Web.Security;
using System.Web.Configuration;

using log4net;

using Blog.Net.Business.BusinessObjects;
using Blog.Net.DataAccess.DataControls;

namespace Blog.Net
{
    public class BlogNetMembershipProvider : MembershipProvider
    {
        #region Constructors

        public BlogNetMembershipProvider() { }

        #endregion

        #region Private Members

        private int newPasswordLength = 8;
        private string applicationName;
        private bool enablePasswordReset;
        private bool enablePasswordRetrieval;
        private bool requiresQuestionAndAnswer;
        private bool requiresUniqueEmail;
        private int maxInvalidPasswordAttempts;
        private int passwordAttemptWindow;
        private MembershipPasswordFormat passwordFormat;
        private MachineKeySection machineKeySection;
        private bool writeExceptionsToLogger;
        private int minRequiredNonAlphanumericCharacters;
        private int minRequiredPasswordLength;
        private string passwordStrengthRegularExpression;
        private static ILog log = log4net.LogManager.GetLogger(typeof(BlogNetMembershipProvider));
        private byte[] validationKey;
        private byte[] decryptionKey;

        #endregion

        #region Public Properties

        public bool WriteExceptionsToLogger
        {
            get { return writeExceptionsToLogger; }
            set { writeExceptionsToLogger = value; }
        }

        public override string ApplicationName
        {
            get { return applicationName; }
            set { applicationName = value; }
        }

        public override bool EnablePasswordReset
        {
            get { return enablePasswordReset; }
        }


        public override bool EnablePasswordRetrieval
        {
            get { return enablePasswordRetrieval; }
        }


        public override bool RequiresQuestionAndAnswer
        {
            get { return requiresQuestionAndAnswer; }
        }


        public override bool RequiresUniqueEmail
        {
            get { return requiresUniqueEmail; }
        }


        public override int MaxInvalidPasswordAttempts
        {
            get { return maxInvalidPasswordAttempts; }
        }


        public override int PasswordAttemptWindow
        {
            get { return passwordAttemptWindow; }
        }


        public override MembershipPasswordFormat PasswordFormat
        {
            get { return passwordFormat; }
        }

        public override int MinRequiredNonAlphanumericCharacters
        {
            get { return minRequiredNonAlphanumericCharacters; }
        }

        public override int MinRequiredPasswordLength
        {
            get { return minRequiredPasswordLength; }
        }

        public override string PasswordStrengthRegularExpression
        {
            get { return passwordStrengthRegularExpression; }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Gets a config setting from the web.config.  If it does not exist then set the default
        /// </summary>
        /// <param name="configValue"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        private string GetConfigValue(string configValue, string defaultValue)
        {
            if (String.IsNullOrEmpty(configValue))
                return defaultValue;

            return configValue;
        }

        private void UpdateFailureCount(string username, string failureType)
        {
            //throw new NotImplementedException();
        }

        /// <summary>
        /// Compares password values based on the MembershipPasswordFormat.
        /// </summary>
        /// <param name="password">provided password</param>
        /// <param name="dbpassword">password in datastore</param>
        /// <returns>true if passwords match</returns>
        private bool CheckPassword(string password, string dbpassword)
        {
            string pass1 = password;
            string pass2 = dbpassword;

            switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Encrypted:
                    pass2 = UnEncodePassword(dbpassword);
                    break;
                case MembershipPasswordFormat.Hashed:
                    pass1 = EncodePassword(password);
                    break;
                default:
                    break;
            }

            if (pass1 == pass2)
                return true;

            return false;
        }

        private string EncodePassword(string password)
        {
            string encodedPassword = password;

            switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Clear:
                    break;
                case MembershipPasswordFormat.Encrypted:
                    encodedPassword =
                      Convert.ToBase64String(EncryptPassword(Encoding.Unicode.GetBytes(password)));
                    break;
                case MembershipPasswordFormat.Hashed:
                    HMACSHA1 hash = new HMACSHA1();
                    hash.Key = validationKey;
                    encodedPassword = Convert.ToBase64String(hash.ComputeHash(Encoding.Unicode.GetBytes(password)));
                    break;
                default:
                    throw new ProviderException("Unsupported password format.");
            }

            return encodedPassword;
        }

        private string UnEncodePassword(string encodedPassword)
        {
            string password = encodedPassword;

            switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Clear:
                    break;
                case MembershipPasswordFormat.Encrypted:
                    password =
                      Encoding.Unicode.GetString(DecryptPassword(Convert.FromBase64String(password)));
                    break;
                case MembershipPasswordFormat.Hashed:
                    throw new ProviderException("Cannot unencode a hashed password.");
                default:
                    throw new ProviderException("Unsupported password format.");
            }

            return password;
        }

        private void WriteToEventLog(Exception e, string action)
        {
            log.Error("An exception occured in Membership Provider.", e);
        }

        private string createRandomPassword()
        {
            int passwordLength = 8;
            string _allowedChars = "abcdefghijkmnopqrstuvwxyzABCDEFGH­ JKLMNOPQRSTUVWXYZ0123456789!@$?";
            Random randNum = new Random();
            char[] chars = new char[passwordLength];
            int allowedCharCount = _allowedChars.Length;

            for (int i = 0; i < passwordLength; i++)
            {
                chars[i] = _allowedChars[(int)((_allowedChars.Length) * randNum.NextDouble())];
            }

            return new string(chars);
        }

        #endregion

        #region Public Methods

        public override void Initialize(string name, NameValueCollection config)
        {
            //
            // Initialize values from web.config.
            //

            if (config == null)
                throw new ArgumentNullException("config");

            if (name == null || name.Length == 0)
                name = "BlogNetMembershipProvider";

            if (String.IsNullOrEmpty(config["description"]))
            {
                config.Remove("description");
                config.Add("description", "Blog.Net Membership provider");
            }

            base.Initialize(name, config);

            applicationName = GetConfigValue(config["applicationName"], System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath);
            maxInvalidPasswordAttempts = Convert.ToInt32(GetConfigValue(config["maxInvalidPasswordAttempts"], "5"));
            passwordAttemptWindow = Convert.ToInt32(GetConfigValue(config["passwordAttemptWindow"], "10"));
            minRequiredNonAlphanumericCharacters = Convert.ToInt32(GetConfigValue(config["minRequiredNonAlphanumericCharacters"], "1"));
            minRequiredPasswordLength = Convert.ToInt32(GetConfigValue(config["minRequiredPasswordLength"], "7"));
            passwordStrengthRegularExpression = Convert.ToString(GetConfigValue(config["passwordStrengthRegularExpression"], ""));
            enablePasswordReset = Convert.ToBoolean(GetConfigValue(config["enablePasswordReset"], "true"));
            enablePasswordRetrieval = Convert.ToBoolean(GetConfigValue(config["enablePasswordRetrieval"], "true"));
            requiresQuestionAndAnswer = Convert.ToBoolean(GetConfigValue(config["requiresQuestionAndAnswer"], "true"));
            requiresUniqueEmail = Convert.ToBoolean(GetConfigValue(config["requiresUniqueEmail"], "true"));
            writeExceptionsToLogger = Convert.ToBoolean(GetConfigValue(config["writeExceptionsToEventLog"], "true"));

            string temp_format = config["passwordFormat"];
            if (temp_format == null)
                temp_format = "Hashed";

            switch (temp_format)
            {
                case "Hashed":
                    passwordFormat = MembershipPasswordFormat.Hashed;
                    break;
                case "Encrypted":
                    passwordFormat = MembershipPasswordFormat.Encrypted;
                    break;
                case "Clear":
                    passwordFormat = MembershipPasswordFormat.Clear;
                    break;
                default:
                    throw new ProviderException("Password format not supported.");
            }

            // Get encryption and decryption key information from the configuration.
            Configuration cfg = WebConfigurationManager.OpenWebConfiguration(System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath);
            machineKeySection = (MachineKeySection)cfg.GetSection("system.web/machineKey");

            switch (passwordFormat)
            {
                case MembershipPasswordFormat.Encrypted: //we need a symmetric key
                    if (!machineKeySection.DecryptionKey.ToLower().Contains("autogenerate"))
                        decryptionKey = Encoding.ASCII.GetBytes(machineKeySection.DecryptionKey);
                    else if (config["decryptionKey"] != null)
                        decryptionKey = Encoding.ASCII.GetBytes(config["decryptionKey"]);
                    else
                        throw new ConfigurationErrorsException("Symmetric key required to encrypt passwords. Are you missing a <decryptionKey> in your provider entry ?");
                    break;
                case MembershipPasswordFormat.Hashed:
                    if (!machineKeySection.ValidationKey.ToLower().Contains("autogenerate"))
                        validationKey = Encoding.ASCII.GetBytes(machineKeySection.ValidationKey);
                    else if (config["validationKey"] != null)
                        validationKey = Encoding.ASCII.GetBytes(config["validationKey"]);
                    else
                        throw new ConfigurationErrorsException("Symmetric key required to hash passwords. Are you missing a <validationKey> in your provider entry ?");
                    break;
            }
        }

        public override bool ChangePassword(string username, string oldPwd, string newPwd)
        {
            if (!ValidateUser(username, oldPwd))
                return false;

            ValidatePasswordEventArgs args = new ValidatePasswordEventArgs(username, newPwd, true);
            OnValidatingPassword(args);

            if (args.Cancel)
                if (args.FailureInformation != null)
                    throw args.FailureInformation;
                else
                    throw new MembershipPasswordException("Change password canceled due to new password validation failure.");

            LoginBO member = LoginDataControl.Instance.GetByUserName(username);

            if (member != null)
            {
                member.Password = EncodePassword(newPwd);
                LoginDataControl.Instance.Save(member);
            }

            return true;
        }

        public override bool ChangePasswordQuestionAndAnswer(string username,
                      string password,
                      string newPwdQuestion,
                      string newPwdAnswer)
        {
            if (!ValidateUser(username, password))
                return false;

            LoginBO member = LoginDataControl.Instance.GetByUserName(username);

            if (member != null)
            {
                member.PasswordQuestion = newPwdQuestion;
                member.PasswordAnswer = EncodePassword(newPwdAnswer);

                LoginDataControl.Instance.Save(member);
            }

            return true;
        }

        public override MembershipUser CreateUser(string username,
                 string password,
                 string email,
                 string passwordQuestion,
                 string passwordAnswer,
                 bool isApproved,
                 object providerUserKey,
                 out MembershipCreateStatus status)
        {
            ValidatePasswordEventArgs args =
              new ValidatePasswordEventArgs(username, password, true);

            OnValidatingPassword(args);

            if (args.Cancel)
            {
                status = MembershipCreateStatus.InvalidPassword;
                return null;
            }


            LoginBO member = LoginDataControl.Instance.GetByEmail(email);

            if (RequiresUniqueEmail && member != null)
            {
                status = MembershipCreateStatus.DuplicateEmail;
                return null;
            }

            MembershipUser user = GetUser(username, false);

            if (user == null)
            {
                DateTime createDate = DateTime.Now;

                member = new LoginBO();

                if (!string.IsNullOrEmpty(ConfigurationManager.AppSettings["ActivateNewUsers"]))
                    member.Active = bool.Parse(ConfigurationManager.AppSettings["ActivateNewUsers"]);
                else
                    member.Active = false;

                member.UserName = username;
                member.Password = EncodePassword(password);
                member.Email = email;
                member.PasswordQuestion = passwordQuestion;
                member.PasswordAnswer = EncodePassword(passwordAnswer);

                if (!string.IsNullOrEmpty(ConfigurationManager.AppSettings["DefaultNewUserRole"]))
                    member.Roles.Add(RoleDataControl.Instance.GetByName(ConfigurationManager.AppSettings["DefaultNewUserRole"]));

                if (LoginDataControl.Instance.Save(member) != null)
                    status = MembershipCreateStatus.Success;
                else
                    status = MembershipCreateStatus.UserRejected;

                return GetUser(username, false);
            }
            else
            {
                status = MembershipCreateStatus.DuplicateUserName;
            }


            return null;
        }

        public override bool DeleteUser(string username, bool deleteAllRelatedData)
        {
            LoginBO member = LoginDataControl.Instance.GetByUserName(username);

            if (member != null)
            {

                LoginDataControl.Instance.Delete(member);

                if (deleteAllRelatedData)
                {
                    //delete all related data
                }
            }

            return true;
        }

        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            MembershipUserCollection users = new MembershipUserCollection();

            totalRecords = (int)LoginDataControl.Instance.GetCountOfAll();

            if (totalRecords > 0)
            {
                IList<LoginBO> logins = LoginDataControl.Instance.GetAll(pageIndex * pageSize, pageSize);

                if (logins != null)
                {
                    foreach (LoginBO login in logins)
                    {
                        users.Add(GetUserFromLogin(login));
                    }
                }
            }

            return users;
        }

        private MembershipUser GetUserFromLogin(LoginBO login)
        {
            MembershipUser user = null;

            if (login != null)
                user = new MembershipUser("BlogNetMembershipProvider", login.UserName, login.Id, login.Email, login.PasswordQuestion, "", login.Active, false, DateTime.MinValue, DateTime.MinValue, DateTime.MinValue, DateTime.MinValue, DateTime.MinValue);

            return user;
        }

        public override int GetNumberOfUsersOnline()
        {
            throw new NotImplementedException();
        }

        public override string GetPassword(string username, string answer)
        {
            if (!EnablePasswordRetrieval)
            {
                throw new ProviderException("Password Retrieval Not Enabled.");
            }

            /*
            if (PasswordFormat == MembershipPasswordFormat.Hashed)
            {
                throw new ProviderException("Cannot retrieve Hashed passwords.");
            }
            */

            string password = "";
            string passwordAnswer = "";
            LoginBO login = LoginDataControl.Instance.GetByUserName(username);

            if (login != null)
            {
                password = login.Password;
                passwordAnswer = login.PasswordAnswer;
            }
            else
            {
                throw new MembershipPasswordException("The supplied user name is not found.");
            }

            if (RequiresQuestionAndAnswer && !CheckPassword(answer, passwordAnswer))
            {
                UpdateFailureCount(username, "passwordAnswer");

                throw new MembershipPasswordException("Incorrect password answer.");
            }

            if (PasswordFormat == MembershipPasswordFormat.Encrypted)
            {
                password = UnEncodePassword(password);
            }

            if (PasswordFormat == MembershipPasswordFormat.Hashed)
            {
                password = createRandomPassword();
                login.Password = EncodePassword(password);
                LoginDataControl.Instance.Save(login);
            }

            return password;
        }

        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            LoginBO login = LoginDataControl.Instance.GetByUserName(username);
            MembershipUser user = GetUserFromLogin(login);

            if (login != null && userIsOnline)
                LoginDataControl.Instance.Save(login);

            return user;
        }

        public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
        {
            LoginBO login = LoginDataControl.Instance.GetById((int)providerUserKey);
            MembershipUser user = GetUserFromLogin(login);

            if (login != null && userIsOnline)
                LoginDataControl.Instance.Save(login);

            return user;
        }

        public override bool UnlockUser(string username)
        {
            throw new NotImplementedException();
        }


        public override string GetUserNameByEmail(string email)
        {
            string username = "";
            LoginBO login = LoginDataControl.Instance.GetByEmail(email);

            if (login == null || login.UserName == null)
                username = "";

            return username;
        }

        public override string ResetPassword(string username, string answer)
        {
            if (!EnablePasswordReset)
            {
                throw new NotSupportedException("Password reset is not enabled.");
            }

            if (answer == null && RequiresQuestionAndAnswer)
            {
                UpdateFailureCount(username, "passwordAnswer");

                throw new ProviderException("Password answer required for password reset.");
            }

            string newPassword = System.Web.Security.Membership.GeneratePassword(newPasswordLength, MinRequiredNonAlphanumericCharacters);

            ValidatePasswordEventArgs args = new ValidatePasswordEventArgs(username, newPassword, true);

            OnValidatingPassword(args);

            if (args.Cancel)
                if (args.FailureInformation != null)
                    throw args.FailureInformation;
                else
                    throw new MembershipPasswordException("Reset password canceled due to password validation failure.");

            LoginBO member = LoginDataControl.Instance.GetByUserName(username);

            if (member != null)
            {
                if (RequiresQuestionAndAnswer && !CheckPassword(answer, member.PasswordAnswer))
                    throw new MembershipPasswordException("Incorrect password answer.");

                member.Password = EncodePassword(newPassword);

                if (LoginDataControl.Instance.Save(member) != null)
                    return newPassword;
                else
                    throw new MembershipPasswordException("The password was not saved.");
            }
            else
            {
                throw new MembershipPasswordException("The supplied user name is not found.");
            }
        }

        public override void UpdateUser(MembershipUser user)
        {
            LoginBO member = LoginDataControl.Instance.GetByUserName(user.UserName);

            if (member != null)
            {
                member.Email = user.Email;
                LoginDataControl.Instance.Save(member);
            }
        }

        /// <summary>
        /// Validates User's password and checks to see if user is approved
        /// </summary>
        /// <param name="userName"></param>
        /// <param name="password"></param>
        /// <returns></returns>
        public override bool ValidateUser(string userName, string password)
        {
            bool isValid = false;

            LoginBO member = LoginDataControl.Instance.GetByUserName(userName);

            if (member != null)
            {
                if (CheckPassword(password, member.Password))
                {
                    if (member.Active)
                    {
                        isValid = true;
                        System.Web.HttpContext.Current.Session["LoginID"] = member.Id;
                    }
                }
                else
                    UpdateFailureCount(userName, "password");
            }

            return isValid;
        }

        public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            throw new NotImplementedException();
        }

        public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            throw new NotImplementedException();
        }

        #endregion
    }
}